/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
 * Copyright 2020 Pelican Mapping
 * http://osgearth.org
 *
 * osgEarth is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */
#ifndef OSGEARTH_TERRAIN_ENGINE_NODE_H
#define OSGEARTH_TERRAIN_ENGINE_NODE_H 1

#include <osgEarth/Map>
#include <osgEarth/Terrain>
#include <osgEarth/TerrainOptions>
#include <osgEarth/TerrainEffect>
#include <osgEarth/TerrainTileModelFactory>
#include <osgEarth/TerrainTileNode>
#include <osgEarth/TerrainEngineRequirements>
#include <osgEarth/TerrainResources>
#include <osgEarth/ShaderUtils>
#include <osgEarth/Progress>
#include <osgEarth/TileKey>
#include <osg/CoordinateSystemNode>
#include <osg/Geode>
#include <osg/NodeCallback>
#include <osg/BoundingBox>
#include <osgUtil/RenderBin>
#include <set>

#define OSGEARTH_ENV_TERRAIN_ENGINE_DRIVER "OSGEARTH_TERRAIN_ENGINE"


namespace osgUtil {
    class CullVisitor;
}

namespace osgEarth
{
    class TerrainEffect;
    class VirtualProgram;

    namespace Util
    {
        class TerrainEngineNodeFactory;
    }

    /**
     * A callback that lets you customize the computed range for a terrain tile.
     * Providing a custom range can let applications better control how paging behaves.
     */
    struct ComputeRangeCallback : public osg::Referenced
    {
        virtual float operator()(osg::Node* node, osg::NodeVisitor& nv) = 0;
    };

    /**
     * Terrain Engine interface. (Pure virtual)
     * Access to the low-level API for the terrain engine, the node graph that
     * renders the planet's surface.
     */
    class /*interface*/ TerrainEngine
    {
    public:
        /**
         * Callback for customizing the TileModel.
         */
        class CreateTileModelCallback : public osg::Referenced
        {
        public:
            virtual void onCreateTileModel(
                TerrainEngineNode* engine,
                TerrainTileModel*  model) = 0;
        };

    public:
        //! Unique ID of the terrain engine instance
        virtual UID getUID() const = 0;

        //! Create a tile data model from the map
        virtual TerrainTileModel* createTileModel(
            const Map* map,
            const TileKey& key,
            const CreateTileManifest& manifest,
            ProgressCallback* progress) = 0;

        //! Access the shared GL resources associated with the terrain engine
        virtual TerrainResources* getResources() const = 0;

        //! Map associated with this terrain engine
        virtual const Map* getMap() const = 0;

        //! Gets the Terrain interface for interacting with the scene graph
        virtual Terrain* getTerrain() = 0;
        virtual const Terrain* getTerrain() const = 0;

        //! Adds a terrain effect
        virtual void addEffect(TerrainEffect* effect) = 0;

        //! Removes a terrain effect
        virtual void removeEffect(TerrainEffect* effect) = 0;

        //! Marks the terrain tiles intersecting the provied extent as invalid
        virtual void invalidateRegion(const GeoExtent& extent) = 0;

        //! Marks the terrain tiles intersecting the provied extent as invalid
        virtual void invalidateRegion(
            const GeoExtent& extent, 
            unsigned minLevel) = 0;

        //! Marks the terrain tiles intersecting the provied extent as invalid
        virtual void invalidateRegion(
            const GeoExtent& extent, 
            unsigned minLevel, 
            unsigned maxLevel) = 0;

        //! Marks the terrain tiles intersecting the provied extent as invalid
        virtual void invalidateRegion(
            const std::vector<const Layer*> layers,
            const GeoExtent& extent,
            unsigned minLevel,
            unsigned maxLevel) = 0;

        //! Marks the terrain tiles intersecting the provied extent as invalid
        virtual void invalidateRegion(
            const std::vector<const Layer*> layers,
            const GeoExtent& extent) = 0;

        //! Access the stateset used to render the terrain.
        virtual osg::StateSet* getSurfaceStateSet() = 0;

        //! Gets the ComputeRangeCallback for this TerrainEngineNode */
        virtual ComputeRangeCallback* getComputeRangeCallback() const = 0;

        //! Sets the ComputeRangeCallback for this TerrainEngineNode */
        virtual void setComputeRangeCallback(ComputeRangeCallback* computeRangeCallback) = 0;

        //! Scene graph root of the terrain engine
        virtual osg::Node* getNode() = 0;

        //! Adds a TileModel creation callback, so you can add custom data
        //! to the TileModel after it's created.
        virtual void addCreateTileModelCallback(CreateTileModelCallback* callback) = 0;
        virtual void removeCreateTileModelCallback(CreateTileModelCallback* callback) = 0;

        //! Create a standalone terrain tile. This method is not used by the
        //! actual terrain engine, but rather exists to support external
        //! processes that need tile geometry. The terrain engine implementation
        //! may or may not provide this capability and will return NULL if
        //! it does not.
        //! @param model Tile model for which to build a tile
        //! @param flags Bitwise-OR of the CreateTileFlags enums (MAY BE IGNORED)
        //! @param referenceLOD If non-zero, adjust the vertex dimensions of the returned tile
        //!        to match this LOD. Example: ask for a tile at tileKey.lod=15, tile is 17x17.
        //!        Specific a referenceLOD=16, tile will be 33x33.
        //! @param area Restrict the processed area to a subarea of the tile model.
        //!        If not valid then the entire tile model will be used.
        virtual osg::Node* createStandaloneTile(
            const TerrainTileModel* model,
            int createTileFlags,
            unsigned referenceLOD,
            const TileKey& subRegion) = 0;
    };

    /**
     * TerrainEngineNode is the base class and interface for map engine implementations.
     *
     * A map engine lives under a MapNode and is responsible for generating the
     * actual geometry representing the Earth.
     */
    class OSGEARTH_EXPORT TerrainEngineNode : 
        public osg::CoordinateSystemNode,
        public TerrainEngine,
        public TerrainEngineRequirements
    {
    public:

        TerrainTileModel* createTileModel(
            const Map* map,
            const TileKey& key,
            const CreateTileManifest& manifest,
            ProgressCallback* progress) override;

        const Map* getMap() const override { return _map.get(); }

        TerrainResources* getResources() const override;

        Terrain* getTerrain() override { return _terrainInterface.get(); }
        const Terrain* getTerrain() const override { return _terrainInterface.get(); }

        void addEffect( TerrainEffect* effect ) override;

        void removeEffect( TerrainEffect* effect ) override;

        void invalidateRegion(const GeoExtent& extent) override {
            invalidateRegion(extent, 0, INT_MAX);
        }

        void invalidateRegion(const GeoExtent& extent, unsigned minLevel) override {
            invalidateRegion(extent, minLevel, INT_MAX);
        }

        virtual void invalidateRegion(const GeoExtent& extent, unsigned minLevel, unsigned maxLevel) override {
            //NOP by default
        }

        virtual void invalidateRegion(
            const std::vector<const Layer*> layers,
            const GeoExtent& extent,
            unsigned minLevel,
            unsigned maxLevel) override
        {
            //NOP by default
        }

        void invalidateRegion(
            const std::vector<const Layer*> layers,
            const GeoExtent& extent) override
        {
            invalidateRegion(layers, extent, 0, INT_MAX);
        }

        virtual osg::StateSet* getSurfaceStateSet() override { return getOrCreateStateSet(); }

        //! Adds a TileModel creation callback, so you can add custom data
        //! to the TileModel after it's created.
        void addCreateTileModelCallback(CreateTileModelCallback* callback) override;
        void removeCreateTileModelCallback(CreateTileModelCallback* callback) override;

        //! Flags supporting the createStandaloneTile method
        enum CreateTileFlags
        {
            CREATE_TILE_INCLUDE_TILES_WITH_MASKS = 1,
            CREATE_TILE_INCLUDE_TILES_WITHOUT_MASKS = 2,
            CREATE_TILE_INCLUDE_ALL = ~0
        };

        //! Create a standalone terrain tile. This method is not used by the
        //! actual terrain engine, but rather exists to support external
        //! processes that need tile geometry. The terrain engine implementation
        //! may or may not provide this capability and will return NULL if
        //! it does not.
        //! @param model Tile model for which to build a tile
        //! @param flags Bitwise-OR of the CreateTileFlags enums (MAY BE IGNORED)
        //! @param referenceLOD If non-zero, adjust the vertex dimensions of the returned tile
        //!        to match this LOD. Example: ask for a tile at tileKey.lod=15, tile is 17x17.
        //!        Specific a referenceLOD=16, tile will be 33x33.
        //! @param area Restrict the processed area to a subarea of the tile model.
        //!        If not valid then the entire tile model will be used.
        virtual osg::Node* createStandaloneTile(
            const TerrainTileModel* model,
            int createTileFlags,
            unsigned referenceLOD,
            const TileKey& subRegion) { return nullptr; }

        //! Shut down the engine
        virtual void shutdown();

        ComputeRangeCallback* getComputeRangeCallback() const override;

        void setComputeRangeCallback(ComputeRangeCallback* computeRangeCallback) override;

        osg::Node* getNode() override {
            return this;
        }

    public:
        class OSGEARTH_EXPORT ModifyTileBoundingBoxCallback : public osg::Referenced
        {
        public:
            virtual void modifyBoundingBox(const TileKey& key, osg::BoundingBox& box) const =0;
        };

        void addModifyTileBoundingBoxCallback(ModifyTileBoundingBoxCallback* callback);
        void removeModifyTileBoundingBoxCallback(ModifyTileBoundingBoxCallback* callback);

    public:

        static TerrainEngineNode* create(const TerrainOptions& options);


    public: // TerrainEngineRequirements

        bool normalTexturesRequired() const { return _requireNormalTextures; }
        bool elevationTexturesRequired() const { return _requireElevationTextures; }
        bool landCoverTexturesRequired() const { return _requireLandCoverTextures; }
        bool parentTexturesRequired() const { return _requireParentTextures; }
        bool elevationBorderRequired() const { return _requireElevationBorder; }
        bool fullDataAtFirstLodRequired() const { return _requireFullDataAtFirstLOD; }

    protected:
        TerrainEngineNode();
        virtual ~TerrainEngineNode();

    public: // osg::Node overrides
        virtual osg::BoundingSphere computeBound() const;
        virtual void traverse( osg::NodeVisitor& );

    protected:
        friend class MapNode;
        friend class TerrainEngineNodeFactory;

        //! Assigns a map to render
        virtual void setMap(const Map* map, const TerrainOptions& options);

        // signals that a redraw is needed because something changed.
        virtual void requestRedraw();

        // Request the state setup be refreshed because something has changed that requires new
        // shaders, state, etc.
        virtual void dirtyState() { }

        osg::ref_ptr<TerrainResources> _textureResourceTracker;

        bool _requireElevationTextures;
        bool _requireNormalTextures;
        bool _requireLandCoverTextures;
        bool _requireParentTextures;
        bool _requireElevationBorder;
        bool _requireFullDataAtFirstLOD;

        osg::ref_ptr<const Map> _map;

    private:
        friend struct TerrainEngineNodeCallbackProxy;

        void onMapModelChanged( const MapModelChange& change );
        virtual void updateTextureCombining() { }

    private:
        bool                               _redrawRequired;
        osg::ref_ptr<Terrain>              _terrainInterface;
        unsigned                           _dirtyCount;
        bool                               _updateScheduled;

        typedef std::vector<osg::ref_ptr<TerrainEffect> > TerrainEffectVector;
        TerrainEffectVector effects_;

        typedef std::vector<osg::ref_ptr<CreateTileModelCallback> > CreateTileModelCallbacks;
        CreateTileModelCallbacks _createTileModelCallbacks;
        mutable Threading::ReadWriteMutex _createTileModelCallbacksMutex;

        typedef std::vector<osg::ref_ptr<ModifyTileBoundingBoxCallback> > ModifyTileBoundingBoxCallbacks;
        ModifyTileBoundingBoxCallbacks _modifyTileBoundingBoxCallbacks;

        osg::ref_ptr<TerrainTileModelFactory> _tileModelFactory;

        osg::ref_ptr<ComputeRangeCallback> _computeRangeCallback;

    public:
        // internal
        void fireModifyTileBoundingBoxCallbacks(const TileKey& key, osg::BoundingBox& box);

        /** Access a typed effect. */
        template<typename T>
        T* getEffect() {
            for(TerrainEffectVector::iterator i = effects_.begin(); i != effects_.end(); ++i ) {
                T* e = dynamic_cast<T*>(i->get());
                if ( e ) return e;
            }
            return 0L;
        }
    };
} // namespace osgEarth

#endif // OSGEARTH_TERRAIN_ENGINE_NODE_H
